using System;
using System.Collections;
using System.IO;
using System.Text.RegularExpressions;
using WPS.Libraries.CssDom;

/*
    CssDom Library
    Copyright (C) 2007-2011  Ferenc Veres

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2.1 of the License, or (at your option) any later version.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

namespace WPS.Modules.StyleLibrary
{
	/// <summary>
	/// General stylesheet manipluation functions for the modules in Style Sheet Library.
	/// </summary>
	public class StyleSheetManager
	{

		public const string JSCode = "designer.rules[{0}] = new designerRule();\ndesigner.rules[{0}].selector = '{1}';\ndesigner.rules[{0}].cssTextIn = '{2}';\n\n";


		#region Find rules in a CssDom object.

		/// <summary>
		/// Search a list of selectors in the given StyleSheet.
		/// Fills Rule and RuleIndex properties for the matcing selectors in the passed StyleRuleSearch array.
		/// Can also create new rules in the css file for missing selectors.
		/// </summary>
		/// <param name="styleSheet">Searching in this loaded style sheet.</param>
		/// <param name="selectors">Array of StyleRuleSearch objects with initialized SelectorsText values.</param>
		/// <param name="createMissing">Create new rule nodes for missing entries. Otherwise missing entries return null in StyleRuleSearch.Rule.</param>
		public static void SearchStyleRules(CSSStyleSheet css, ref StyleRuleSearch[] rules, bool createMissing)
		{
			// Loop on all rules in the CSS file, find related rules.
			for(int i = 0; i < css.cssRules.length; i++)
			{
				CSSStyleRule rule = (CSSStyleRule)css.cssRules[i];

				string mainSelector = rule.selectorText.Split(',')[0].Trim();

				// Loop searched rules: do we need this rule?
				for(int j = 0; j < rules.Length; j++)
				{
					string mainSearch = rules[j].SelectorText.Split(',')[0].Trim();

					// Rule in results matching the current rule in the css file?
					if(mainSearch.ToLower() == mainSelector.ToLower())
					{
						// Rule found, store it's pointers
						rules[j].Rule = (CSSStyleRule)rule;
						rules[j].RuleIndex = i;
					}
				}
			}

			// Create new rules for missing selectors if necessary
			if(createMissing)
			{
				// Loop on all searched rules.
				for(int i = 0; i < rules.Length; i++)
				{
					// Searched rule was not found in stylesheet?
					if(rules[i].Rule == null)
					{
						int index = -1;

						if(i == 0)
						{
							if(rules.Length == 1)
							{
								// Working with 1 rule, add to the end.
								index = css.cssRules.length;
							}
							else
							{
								// If first rule missing, find any found rules to insert before
								int findIndex = 1;
								bool found = false;
								do
								{
									if(rules[findIndex].Rule != null)
									{
										index = rules[findIndex].RuleIndex;
										found = true;
									}
									findIndex++;
								}
								while(!found && findIndex < rules.Length);

								// Not found any rules, start inserting at end.
								if(!found)
								{
									index = css.cssRules.length;
								}
							}
						}
						else
						{
							// Get the previous item's index and add 1 for new insert point
							index = rules[i - 1].RuleIndex + 1;
						}

						// Create an empty rule for us in the StyleSheet
						rules[i].RuleIndex = css.insertRule(rules[i].SelectorText + " { }", index);
						rules[i].Rule = (CSSStyleRule)css.cssRules[rules[i].RuleIndex];

						// Later indexes must be increased to reflect the insertion
						for(int k = i + 1; k < rules.Length; k++)
						{
							if(rules[k].Rule != null)
							{
								rules[k].RuleIndex++;
							}
						}
					}

				}
			}
		}

		#endregion

		#region Delete rules from a CssDom object.

		/// <summary>
		/// Delete rules from a CssDom stylesheet.
		/// </summary>
		/// <param name="css">StyleSheet to delete from.</param>
		/// <param name="rules">Array of StyleRuleSearches to "seek and destroy".</param>
		public static void DeleteStyleRules(CSSStyleSheet css, ref StyleRuleSearch[] rules)
		{
			StyleSheetManager.SearchStyleRules(css, ref rules, false);
			StyleRuleSearchComparer comparer = new StyleRuleSearchComparer();

			// Sort rules by Rule Index to delete backwards.
			ArrayList sortedRules = new ArrayList();

			for(int i = 0; i < rules.Length; i++)
			{
				if(rules[i].Rule != null)
				{
					sortedRules.Add(rules[i]);
				}
			}
			sortedRules.Sort(comparer);

			// Delete rules.
			for(int i = sortedRules.Count - 1; i >= 0; i--)
			{
				css.cssRules.deleteRule(((StyleRuleSearch)sortedRules[i]).RuleIndex);
			}
		}

		#endregion

		#region Repace CSS Selectors in a CssDom object.

		/// <summary>
		/// Replaces selectors in CssDom. Each rule's selector in "rules" is 
		/// replaced with an entry in renamedRules array.
		/// </summary>
		/// <param name="rules">Existing CSS rules in the DOM.</param>
		/// <param name="renamedRules">New selector content.</param>
		public static void RenameRules(ref StyleRuleSearch[] rules, string[] renamedRules)
		{
			for(int i = 0; i < rules.Length; i++)
			{
				rules[i].Rule.selectorText = renamedRules[i];
			}
		}

		#endregion

		#region Update CSS rules in a css file

		/// <summary>
		/// Load CSS file, update the related rules (properties) and save the file.
		/// </summary>
		/// <param name="fileName">CSS file to modify, full path.</param>
		/// <param name="editedRules">String of CSS rules with correct selectors and new content.</param>
		/// <param name="removeEmptyRules">True will omit saving the rules which end up as empty.</param>
		public static void UpdateCssFile(string fileName, string editedRules, bool removeEmptyRules)
		{
			UpdateCssFile(fileName, editedRules, removeEmptyRules, null);
		}

		/// <summary>
		/// Load CSS file, update the related rules (properties and selectors) and save the file.
		/// </summary>
		/// <param name="fileName">CSS file to modify, full path.</param>
		/// <param name="editedRules">String of CSS rules with correct selectors and new content.</param>
		/// <param name="removeEmptyRules">True will omit saving empty rules of editedRules. First (main) rule is always saved). which end up as empty.</param>
		/// <param name="renamedRules">Array of new CSS selector texts to replace existing selectors. Must match count of rules in editedRules.</param>
		public static void UpdateCssFile(string fileName, string editedRules, bool removeEmptyRules, string[] renamedRules)
		{
			// Load destination CSS file
			CSSStyleSheet css = new CSSStyleSheet();
			css.Load(fileName);

			// Parse the posted style rules.
			CSSStyleSheet editedCss = new CSSStyleSheet();
			editedCss.LoadString(editedRules);

			// Create rule search array in size of posted rules.
			StyleRuleSearch[] rules = new StyleRuleSearch[editedCss.cssRules.length];

			// Initialize search rules from the posted declarations
			for(int i = 0; i < editedCss.cssRules.length; i++)
			{
				CSSStyleRule editedRule = (CSSStyleRule)editedCss.cssRules[i];
				rules[i] = new StyleRuleSearch(editedRule.selectorText);
			}

			// Searches these rules in wysiwyg.css css-dom
			StyleSheetManager.SearchStyleRules(css, ref rules, true);

			// Overwrite the found or created rules with new content.
			for(int i = 0; i < editedCss.cssRules.length; i++)
			{
				// Overwrite property definitions
				CSSStyleRule editedRule = (CSSStyleRule)editedCss.cssRules[i];
				rules[i].Rule.style.cssText = editedRule.style.cssText;
			}

			// Renamed styles, replace selectors.
			if(renamedRules != null)
			{
				if(renamedRules.Length != editedCss.cssRules.length)
				{
					throw new Exception("StyleLib: Rename rules array length (" + renamedRules.Length + ") not equal to edited rules length (" + editedCss.cssRules.length + ").");
				}

				for(int i = 0; i < editedCss.cssRules.length; i++)
				{
					// Overwrite selectors
					CSSStyleRule editedRule = (CSSStyleRule)editedCss.cssRules[i];
					rules[i].Rule.selectorText = renamedRules[i];
				}
			}

			if(removeEmptyRules)
			{
				StyleRuleSearchComparer comparer = new StyleRuleSearchComparer();

				// Sort rules by Rule Index to delete backwards. (skip main rule)
				ArrayList sortedRules = new ArrayList();

				for(int i = 1; i < rules.Length; i++)
				{
					if(rules[i].Rule != null)
					{
						sortedRules.Add(rules[i]);
					}
				}
				sortedRules.Sort(comparer);

				// Delete empty rules backwards
				for(int i = sortedRules.Count - 1; i >= 0; i--)
				{
					if(((StyleRuleSearch)sortedRules[i]).Rule.style.cssText == "")
					{
						css.cssRules.deleteRule(((StyleRuleSearch)sortedRules[i]).RuleIndex);
					}
				}

			}

			// Create output directory if needed
			string dir = fileName.Substring(0, fileName.LastIndexOf("\\"));
			if(!Directory.Exists(dir))
			{
				Directory.CreateDirectory(dir);
			}
			css.Save(fileName);
		}

		#endregion

		#region Load class names from CSS 

		/// <summary>
		/// Load a special css file (wps's wysiwyg.css) and parse known classes from it.
		/// </summary>
		/// <param name="styleFile">Full path to the wysiwyg.css file.</param>
		public static StyleNameList LoadAllStyles(string styleFile)
		{
			StyleNameList classNames = new StyleNameList();

			CSSStyleSheet css = new CSSStyleSheet();
			if(css.Load(styleFile))
			{
				Regex tableClassRx = new Regex("^table\\.([0-9a-z-_]+)$", RegexOptions.IgnoreCase);
				Regex imgClassRx = new Regex("^img\\.([0-9a-z-_]+)$", RegexOptions.IgnoreCase);
				Regex textBoxClassRx = new Regex("^div\\.([0-9a-z-_]+)$", RegexOptions.IgnoreCase);
				Regex fontClassRx = new Regex("^\\.([0-9a-z-_]+)", RegexOptions.IgnoreCase);

				for(int i = 0; i < css.cssRules.length; i++)
				{
					CSSStyleRule rule = (CSSStyleRule)css.cssRules[i];

					if(tableClassRx.IsMatch(rule.selectorText))
					{
						// TABLE CLASSES
						Match match = tableClassRx.Match(rule.selectorText);
						classNames.Tables.Add(match.Groups[1].Value);
					}
					else if(imgClassRx.IsMatch(rule.selectorText))
					{
						// IMAGE CLASSES
						Match match = imgClassRx.Match(rule.selectorText);
						classNames.Images.Add(match.Groups[1].Value);
					}
					else if(textBoxClassRx.IsMatch(rule.selectorText))
					{
						// TEXT BOX CLASSES
						Match match = textBoxClassRx.Match(rule.selectorText);
						classNames.TextBoxes.Add(match.Groups[1].Value);
					}
					else if(fontClassRx.IsMatch(rule.selectorText))
					{
						// FONT CLASSES
						Match match = fontClassRx.Match(rule.selectorText);
						classNames.Fonts.Add(match.Groups[1].Value);
					}
				}

			}

			return classNames;
		}

		#endregion

		#region Designer functions

		/// <summary>
		/// Return the found styles as JavaScript array definition code snipset for Style Designer modules.
		/// </summary>
		/// <param name="rules"></param>
		/// <returns></returns>
		public static string CreateJSArray(StyleRuleSearch[] rules)
		{
			string js = "";
			int r = 0;

			for(int i = 0; i < rules.Length; i++)
			{
				if(rules[i].Rule != null)
				{
					// Create rule settings with cssText content.
					js += String.Format(StyleSheetManager.JSCode, r,
							StyleSheetManager.JsEsc(rules[i].Rule.selectorText),
							StyleSheetManager.JsEsc(rules[i].Rule.style.cssText));
					r++;
				}
				else
				{
					// Create empty rule
					js += String.Format(StyleSheetManager.JSCode, r,
							StyleSheetManager.JsEsc(rules[i].SelectorText),
							"");
					r++;
				}
			}
			return js;
		}

		public static string JsEsc(string str)
		{
			string s = str.Replace("\r", "");
			s = s.Replace("\n", "");
			s = s.Replace("'", "\\'");
			return s;
		}

		#endregion

	}
}